Skip to content

Handle Blockscout PRO API credit-exhaustion as HTTP 402#399

Merged
akolotov merged 7 commits into
mainfrom
claude/tender-kapitsa-bc6c03
Jun 5, 2026
Merged

Handle Blockscout PRO API credit-exhaustion as HTTP 402#399
akolotov merged 7 commits into
mainfrom
claude/tender-kapitsa-bc6c03

Conversation

@akolotov
Copy link
Copy Markdown
Collaborator

@akolotov akolotov commented Jun 5, 2026

What & why

Closes #393.

The Blockscout PRO API meters access by credits and returns HTTP 402 with body {"error": "Out of credits"} when the daily allowance for the server's API key is exhausted. Previously this was treated like a generic transient upstream failure, so it could be retried and surfaced as an opaque error. This change makes credit exhaustion a distinct, clearly-labeled signal that propagates immediately (no retries) and is surfaced to REST clients as 402 Payment Required.

Changes

  • Phase 1 — Shared HTTP core (75d3d5e): Added a dedicated CreditsExhaustedError (a direct Exception subclass, not a ValueError) alongside ResponseTooLargeError. The shared _make_blockscout_http_request core now maps any 402 to CreditsExhaustedError at the start of its httpx.HTTPStatusError handler — before enrichment and outside the retry set — so it applies uniformly to all PRO API helpers (make_blockscout_request, make_blockscout_post_request, make_metadata_request) and never triggers a retry. Raises: docstrings updated on the three public helpers.
  • Phase 2 — REST mode (4460cf4): The handle_rest_errors decorator maps CreditsExhaustedError to JSONResponse({"error": ...}, status_code=402), mirroring the existing ResponseTooLargeError → 413 shape and ordered before the generic catch-all.
  • Phase 3 — Documentation (9d84fe9): SPEC.md (§7 enrichment sentence, §8 new "Credit Exhaustion" subsection, error-semantics revision), API.md (new 402 Payment Required error category), AGENTS.md (tests/api/ tree). Also added three empty __init__.py files (tests/api/, tests/tools/, tests/tools/transaction/) to resolve a test module-name collision between the new tests/api/test_helpers.py and the pre-existing tests/tools/transaction/test_helpers.py.
  • Phase 4 — Version bump (33881cf): 0.16.0.dev140.16.0.dev15 in pyproject.toml, blockscout_mcp_server/__init__.py, and server.json.

Testing

  • Unit tests: 638 passed (new core-helper tests assert CreditsExhaustedError, exactly-once invocation, and no retry; REST helper + route tests assert the 402 mapping and handler ordering).
  • Integration regression (timeout-protected runner): 85 passed, 0 failed, 0 skipped, 0 timed out.
  • ruff check . and ruff format --check .: clean.

Reviewer notes

  • The 402 path itself is not integration-testable — credit exhaustion cannot be forced on the shared deployment key — so it is covered by unit tests only. The integration run guards against regressions in the shared _make_blockscout_http_request core that this change touches.
  • The mapping is plan-agnostic: paid-plan keys are allowed through at a negative balance and simply never receive 402 from this gate, which is acceptable (see SPEC.md §8).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Detects PRO API credit exhaustion and surfaces it as HTTP 402; primary requests may hard-fail while side requests can degrade with partial results and notes.
  • Documentation

    • Added guidance for HTTP 402 (Payment Required) explaining credit exhaustion behavior and retry/top-up guidance.
  • Tests

    • Added extensive tests covering 402 handling and graceful degradation across API and tooling flows.
  • Chores

    • Version bumped to 0.16.0.dev15.

akolotov and others added 4 commits June 4, 2026 21:55
…core

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

Warning

Review limit reached

@akolotov, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 16 minutes and 56 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f2cfb311-d474-4c8d-80ac-3c8a9d0f8073

📥 Commits

Reviewing files that changed from the base of the PR and between c4aa4e9 and ade9404.

📒 Files selected for processing (1)
  • SPEC.md

Walkthrough

Adds CreditsExhaustedError and maps Blockscout PRO HTTP 402 to it in the shared request core (no retry), returns HTTP 402 from the REST handler, updates docs and versions, and adds tests covering GET/POST/metadata and tool-level graceful degradation.

Changes

HTTP 402 Credit Exhaustion Error Handling

Layer / File(s) Summary
Exception type and HTTP request core
blockscout_mcp_server/tools/common.py
Defines CreditsExhaustedError and maps PRO HTTP 402 to it in _make_blockscout_http_request; updates docstrings for GET/POST/metadata helpers.
REST error handler integration
blockscout_mcp_server/api/helpers.py
Imports CreditsExhaustedError and extends handle_rest_errors to return JSON response with HTTP status 402 when raised.
Core HTTP request tests
tests/tools/test_common_blockscout_request.py, tests/tools/test_common_metadata.py, tests/tools/test_common_post_request.py
New tests verify HTTP 402 causes CreditsExhaustedError and no retry for GET, POST, and metadata helpers.
REST handler and routes tests
tests/api/test_api_helpers.py, tests/api/test_routes.py
Adds tests asserting handle_rest_errors maps CreditsExhaustedError→402 and updates route error-handling parametrization to include 402.
Tool-level graceful degradation tests
tests/tools/address/test_get_address_info.py, tests/tools/address/test_get_address_info_metadata.py, tests/tools/block/test_get_block_info.py, tests/tools/transaction/test_get_transaction_info.py
New tests assert tools soft-fail side-request 402s into notes/partial results while primary-request 402s propagate as errors.
Documentation and version updates
SPEC.md, API.md, AGENTS.md, blockscout_mcp_server/__init__.py, pyproject.toml, server.json
SPEC.md documents 402→CreditsExhaustedError mapping and no-retry behavior; API.md documents 402 Payment Required; AGENTS.md updated; package and server version bumped to 0.16.0.dev15.

Possibly related issues

Possibly related PRs

  • blockscout/mcp-server#397: Prerequisite refactor routing metadata through the shared HTTP core that this PR extends with 402→CreditsExhaustedError.
  • blockscout/mcp-server#376: Related work on PRO metadata routing/auth in the shared request core.
  • blockscout/mcp-server#364: Prior refactor of the shared _make_blockscout_http_request core that this change special-cases for 402.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing distinct HTTP 402 handling for Blockscout PRO credit exhaustion as a focused, specific improvement.
Linked Issues check ✅ Passed All objectives from issue #393 are met: CreditsExhaustedError created and properly subclasses Exception; 402 mapped in shared _make_blockscout_http_request core; REST error handling updated; tests and documentation added.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #393 scope: error-path handling, distinct error type, core HTTP mapping, REST mode handling, documentation, and tests. No out-of-scope changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/tender-kapitsa-bc6c03

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@akolotov akolotov self-assigned this Jun 5, 2026
@akolotov akolotov changed the title Handle Blockscout PRO API credit-exhaustion as HTTP 402 (CreditsExhaustedError) Handle Blockscout PRO API credit-exhaustion as HTTP 402 Jun 5, 2026
Revert the three __init__.py files added in Phase 3 and rename
tests/api/test_helpers.py -> tests/api/test_api_helpers.py to
disambiguate it from tests/tools/transaction/test_helpers.py under
pytest's prepend import mode. This keeps the test tree flat and
consistent (no partial-package asymmetry) instead of relying on a
subset of __init__.py files. Update the AGENTS.md tree accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@akolotov akolotov marked this pull request as ready for review June 5, 2026 05:09
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
SPEC.md (1)

605-616: 💤 Low value

Documentation captures the essential credit-exhaustion semantics.

The new section correctly documents the 402 mapping, no-retry behavior, plan-agnostic design, and surface-mode differences. All claims are verified by the implementation (context snippets confirm the shared-core mapping, REST handler 402 response, and composite-tool downgrade).

The composite-tool paragraph (lines 615-616) is somewhat detailed but provides important context for users debugging partial-success scenarios. Based on learnings, SPEC.md prefers concise high-level descriptions over detailed technical explanations.

Optional: Condense the composite-tool paragraph

If you prefer tighter prose, the composite-tool behavior could be condensed:

-   Note on composite tools: several tools issue multiple PRO API requests concurrently via `asyncio.gather(..., return_exceptions=True)`, with exactly one hard-fail *primary* request whose exception is re-raised and surfaces directly, plus one or more optional *side* requests whose isolated failures are each downgraded to a null field plus an explanatory note. This applies to `get_address_info` (primary: address-info; side: metadata and first-transaction), `get_block_info` with `include_transactions=True` (primary: block details; side: the transactions list), and `get_transaction_info` (primary: the transaction; side: account-abstraction user operations). Because the side requests share the same `_make_blockscout_http_request` core, a `402` raised by a side request *alone* is mapped to `CreditsExhaustedError` and then absorbed into a note (partial data is still returned) rather than surfaced as a hard error. Under genuine credit exhaustion, however, every request — including the primary one — is rejected, so the `CreditsExhaustedError` still reaches the client through the primary request's re-raise.
+   Note on composite tools: tools that issue concurrent requests (e.g., `get_address_info`) map a `402` from optional side requests into a note with partial data returned. Under genuine credit exhaustion, the primary request also fails with `CreditsExhaustedError`, so the error still surfaces.

This preserves the key insight (side-request downgrade vs primary hard-fail) without enumerating every affected tool.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SPEC.md` around lines 605 - 616, The composite-tool paragraph is too
detailed; trim it to a concise high-level statement that side requests are
downgraded to notes while the primary request hard-fails on
CreditsExhaustedError. Update the paragraph referencing the shared core
_make_blockscout_http_request and the CreditsExhaustedError mapping, and keep
the examples minimal by mentioning the affected helpers (get_address_info,
get_block_info, get_transaction_info) only as a short illustrative list rather
than enumerating primary vs side per tool.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@SPEC.md`:
- Around line 605-616: The composite-tool paragraph is too detailed; trim it to
a concise high-level statement that side requests are downgraded to notes while
the primary request hard-fails on CreditsExhaustedError. Update the paragraph
referencing the shared core _make_blockscout_http_request and the
CreditsExhaustedError mapping, and keep the examples minimal by mentioning the
affected helpers (get_address_info, get_block_info, get_transaction_info) only
as a short illustrative list rather than enumerating primary vs side per tool.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b7357dc2-0bf9-4d75-b451-af9b2b850d15

📥 Commits

Reviewing files that changed from the base of the PR and between d67888a and 073d65a.

📒 Files selected for processing (17)
  • AGENTS.md
  • API.md
  • SPEC.md
  • blockscout_mcp_server/__init__.py
  • blockscout_mcp_server/api/helpers.py
  • blockscout_mcp_server/tools/common.py
  • pyproject.toml
  • server.json
  • tests/api/test_api_helpers.py
  • tests/api/test_routes.py
  • tests/tools/address/test_get_address_info.py
  • tests/tools/address/test_get_address_info_metadata.py
  • tests/tools/block/test_get_block_info.py
  • tests/tools/test_common_blockscout_request.py
  • tests/tools/test_common_metadata.py
  • tests/tools/test_common_post_request.py
  • tests/tools/transaction/test_get_transaction_info.py

…tion

Replace four verbose paragraphs (~350 words) with two concise sentences
covering the shared-core mapping, no-retry behaviour, client surfaces, and
composite-tool primary-vs-side semantics (~60 words).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SPEC.md`:
- Line 607: The spec text at `_make_blockscout_http_request` is inconsistent: it
currently says “any 402 response” maps to `CreditsExhaustedError` but earlier
(line 232) limits this to responses with body `{"error":"Out of credits"}`;
update the `_make_blockscout_http_request` section to match the earlier contract
by specifying that only HTTP 402 responses whose body/error field equals `"Out
of credits"` (or an equivalent structured indicator) are mapped to
`CreditsExhaustedError` (and thereby propagate without retries to REST and
native MCP clients), rather than mapping every 402.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 439a5fdf-8063-4708-8d72-09d25c046efc

📥 Commits

Reviewing files that changed from the base of the PR and between 073d65a and c4aa4e9.

📒 Files selected for processing (1)
  • SPEC.md

Comment thread SPEC.md Outdated
Line 607 said "any 402 response" which contradicts the contract at line 232
(only HTTP 402 with body {"error":"Out of credits"} maps to
CreditsExhaustedError). Align the description to match the authoritative
contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@akolotov akolotov merged commit 73ba60d into main Jun 5, 2026
8 checks passed
@akolotov akolotov deleted the claude/tender-kapitsa-bc6c03 branch June 5, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle Blockscout PRO API credit-exhaustion as a distinct error

1 participant